Unit myGPSUtils;
  {$M+}
  {$H+}
  {$IFDEF FPC}
  {$MODE delphi}
  {$ENDIF}

Interface

Uses Classes, DateUtils, Registry, SysUtils, SyncObjs, Windows;

Const
    MaxBaud = 38400;
    MinBaud = 4800;

Type
    TFixQuality = (fqInvalid, fqGPSFix, fqDGPSFix, fqPPSFix,
        fqRealTimeKinematic, fqFloatRTK, fqEstimatedDeadReckoning,
        fqManualInputMode, fqSimulationMmode);
    TFixMode    = (fmNoFix, fm2DFix, fm3DFix);

    TDegree = Class(TObject)
    Private
        FValue: Double;
        FDegree: Integer;
        FMinute: Integer;
        FSecond: Double;
        Function GetDegreeAsStr: String;
    Public
        Constructor Create;
        Procedure SetDegreeValue(ANMEADegtString: String);
        Property AsString: String read GetDegreeAsStr;
        Property Value: String;
    End;

    TGPSInfo = Class(TObject)
    Private
        FGPSTime: TDateTime;
        FTimeZone: Double;
        FTimeSynced: Boolean;
        FFixed: Boolean;
        FFixQuality: TFixQuality;
        FFixMode: TFixMode;
        FSatNumbers: Integer;
        FGPSLock: SyncObjs.TCriticalSection;
        FLatitude: TDegree;
        FLongtitude: TDegree;
        FDirection: TDegree;
        FAltitude: Double;
        FNorthOrSouth: String;
        FWestOrEast: String;
        FSouthStr: String;
        FEastStr: String;
        FNorthStr: String;
        FWestStr: String;
        FKMHStr: String;
        FMSStr: String;
        FKMStr: String;
        FMStr: String;
        FSpeed: Double;
        Function GetLatAsStr: String;
        Function GetLngAsStr: String;
        Function GetAltAsStr: String;
        Function GetDirAsStr: String;
        Function GetSpdAsStr: String;
        Function GetSpdKMH: String;
        Function GetSpdMS: String;
        Procedure DecodeVTG(Params: TStringList);
        Procedure DecodeGLL(Params: TStringList);
        Procedure DecodeGSA(Params: TStringList);
        Procedure DecodeGGA(Params: TStringList);
        Procedure DecodeRMC(Params: TStringList);
        Procedure SetLatitude(LatStr, DirStr: String);
        Procedure SetLongtitude(LngStr, DirStr: String);
        Procedure SetDirection(DirStr: String);
        Procedure SetAltitude(AltStr: String);
        Procedure SetTime(TimeStr: String);
        Procedure SetDate(DateStr: String);
        Procedure SetSpdKMH(SpdStr: String);
        Procedure SetSpdKNT(SpdStr: String);
    Public
        Constructor Create;
        Destructor Destroy; Override;
        Procedure Parse(ANMEAString: String);
        Property East: String read FEastStr write FEastStr;
        Property West: String read FWestStr write FWestStr;
        Property North: String read FNorthStr write FNorthStr;
        Property South: String read FSouthStr write FSouthStr;
        Property KMH: String read FKMHStr write FKMHStr;
        Property KM: String read FKMStr write FKMStr;
        Property MS: String read FMSStr write FMSStr;
        Property M: String read FMStr write FMStr;
        Procedure SetEast(WEStr: String);
        Procedure SetWest(WEStr: String);
        Procedure SetKMH(KMHStr: String);
        Procedure SetMS(MSStr: String);
        Procedure SetKM(KMStr: String);
        Procedure SetM(MStr: String);
        Procedure SetSouth(NSStr: String);
        Procedure SetNorth(NSStr: String);
        Property Latitude: String read GetLatAsStr;
        Property Longtitude: String read GetLngAsStr;
        Property Altitude: String read GetAltAsStr;
        Property Direction: String read GetDirAsStr;
        Property Speed: String read GetSpdAsStr;
        Property SpeedKMH: String read GetSpdKMH;
        Property SpeedMS: String read GetSpdMS;
        Property TimeSynced: Boolean read FTimeSynced;
        Property GPSTime: TDateTime read FGPSTime;
        Property TimeZone: Double read FTimeZone write FTimeZone;
        Property GPSLock: SyncObjs.TCriticalSection read FGPSLock write FGPSLock;
    End;

Procedure EnumComPort(Var ComPortList: TStringList);
Function ChkSumNMEA(Const S: String): String;
Function IsValidNMEA(Const S: String): Boolean;

Implementation

Function ChkSumNMEA(Const S: String): String;
Var
    I, N: Byte;
Begin
    N := Ord(S[2]);
    For I := 3 To Length(S) Do
    Begin
        If S[I] = '*' Then Exit(IntToHex(N, 2));
        N := N xor Ord(S[I]);
    End;
End;

Function IsValidNMEA(Const S: String): Boolean;
Var
    N: Integer;
Begin
    N := Pos('*', S);
    Exit(Copy(S, N + 1, 2) = ChkSumNMEA(S));
End;

Constructor TDegree.Create;
Begin
    FDegree := 0;
    FMinute := 0;
    FSecond := 0.0;
    FValue := 0.0;
End;

Procedure TDegree.SetDegreeValue(ANMEADegtString: String);
Begin
    While Length(ANMEADegtString) < 10 Do ANMEADegtString :=
            '0' + ANMEADegtString;
    If (ANMEADegtString <> '') Then
    Begin
        FDegree := StrToInt(Copy(ANMEADegtString, 1, 3));
        FMinute := StrToInt(Copy(ANMEADegtString, 4, 2));
        FSecond := StrToFloat('0' + Copy(ANMEADegtString, 6, MaxInt)) * 60;
        FValue  := FDegree + (FMinute + FSecond * 60) * 60;
    End;
End;

Function TDegree.GetDegreeAsStr: String;
Begin
    Exit(FormatFloat('000', FDegree) + #$a1 + #$e3 +
         FormatFloat('00', FMinute) + #$27 +
         FormatFloat('00.000', FSecond) + #$22);
End;

Constructor TGPSInfo.Create;
Begin
    FLatitude := TDegree.Create;
    FLongtitude := TDegree.Create;
    FDirection := TDegree.Create;
    FAltitude := 0.0;
    FNorthStr := 'N ';
    FEastStr := 'E ';
    FSouthStr := 'S ';
    FWestStr := 'W ';
    FNorthOrSouth := 'N';
    FWestOrEast := 'E';
    FKMHStr := 'KM/H';
    FMSStr := 'M/S';
    FKMStr := 'km';
    FMStr := 'm';
    FSpeed := 0.0;
    FGPSTime := Now;
    FTimeZone := 8.0;
    FTimeSynced := False;
    FFixed := False;
    FFixQuality := fqInvalid;
    FFixMode := fmNoFix;
    FSatNumbers := 0;
    FGPSLock := SyncObjs.TCriticalSection.Create;
End;

Destructor TGPSInfo.Destroy;
Begin
    FreeAndNil(FGPSLock);
    FreeAndNil(FLatitude);
    FreeAndNil(FLongtitude);
    FreeAndNil(FDirection);
End;

Function TGPSInfo.GetLngAsStr: String;
Begin
    If FWestOrEast = 'W' Then Exit(FWestStr + FLongtitude.AsString);
    Exit(FEastStr + FLongtitude.AsString);
End;

Function TGPSInfo.GetAltAsStr: String;
Begin
    If FAltitude < 9999.99 Then Exit(FormatFloat('0.00', FAltitude) + FMStr);
    Exit(FormatFloat('0.00', FAltitude / 1000) + FKMStr);
End;

Function TGPSInfo.GetLatAsStr: String;
Begin
    If FNorthOrSouth = 'S' Then Exit(FSouthStr + FLatitude.AsString);
    Exit(FNorthStr + FLatitude.AsString);
End;

Function TGPSInfo.GetDirAsStr: String;
Begin
    Exit(FDirection.AsString);
End;

Function TGPSInfo.GetSpdKMH: String;
Begin
    Exit(FormatFloat('0', FSpeed) + FKMHStr);
End;

Function TGPSInfo.GetSpdMS: String;
Begin
    Exit(FormatFloat('0', FSpeed * 1000 / 3600) + FMSStr);
End;

Function TGPSInfo.GetSpdAsStr: String;
Begin
    If FSpeed >= 1000 Then Exit(GetSpdMS);
    Exit(GetSpdKMH);
End;

Procedure TGPSInfo.SetKM(KMStr: String);
Begin
    If Trim(KMStr) <> '' Then FKMStr := (Trim(KMStr));
End;

Procedure TGPSInfo.SetM(MStr: String);
Begin
    If Trim(MStr) <> '' Then FMStr := (Trim(MStr));
End;

Procedure TGPSInfo.SetNorth(NSStr: String);
Begin
    If Trim(NSStr) <> '' Then FNorthStr := (NSStr);
End;

Procedure TGPSInfo.SetSouth(NSStr: String);
Begin
    If Trim(NSStr) <> '' Then FSouthStr := (NSStr);
End;

Procedure TGPSInfo.SetEast(WEStr: String);
Begin
    If Trim(WEStr) <> '' Then FEastStr := (WEStr);
End;

Procedure TGPSInfo.SetWest(WEStr: String);
Begin
    If Trim(WEStr) <> '' Then FWestStr := (WEStr);
End;

Procedure TGPSInfo.SetKMH(KMHStr: String);
Begin
    If Trim(KMHStr) <> '' Then FKMHStr := (Trim(KMHStr));
End;

Procedure TGPSInfo.SetMS(MSStr: String);
Begin
    If Trim(MSStr) <> '' Then FMSStr := (Trim(MSStr));
End;

Procedure TGPSInfo.SetLatitude(LatStr, DirStr: String);
Begin
    FLatitude.SetDegreeValue(LatStr);
    FNorthOrSouth := DirStr;
End;

Procedure TGPSInfo.SetLongtitude(LngStr, DirStr: String);
Begin
    FLongtitude.SetDegreeValue(LngStr);
    FWestOrEast := DirStr;
End;

Procedure TGPSInfo.SetDirection(DirStr: String);
Var
    iDeg: Integer;
    fMin: Double;
Begin
    iDeg := StrToInt(FormatFloat('0', StrToFloat(DirStr)));
    fMin := (StrToFloat(DirStr) - iDeg) * 60;
    FDirection.SetDegreeValue(IntToStr(iDeg) + FormatFloat('00.0000', fMin));
End;

Procedure TGPSInfo.SetAltitude(AltStr: String);
Begin
    FAltitude := StrToFloat(AltStr);
End;

Procedure TGPSInfo.SetSpdKMH(SpdStr: String);
Begin
    FSpeed := StrToFloat(SpdStr);
End;

Procedure TGPSInfo.SetSpdKNT(SpdStr: String);
Begin
    FSpeed := StrToFloat(SpdStr) * 1.854;
End;

Procedure TGPSInfo.SetTime(TimeStr: String);
Var
    Temp: TDateTime;
    GpsHr, GpsMi, GpsSc: Integer;
    GpsMs, IncMins: Integer;
    ST: TSystemTime;
Begin
    GpsHr := StrToInt(Copy(TimeStr, 1, 2));
    GpsMi := StrToInt(Copy(TimeStr, 3, 2));
    GpsSc := StrToInt(Copy(TimeStr, 5, 2));
    GpsMs := StrToInt(Copy(TimeStr, 8, 3));
    FGPSTime := ComposeDateTime(FGPSTime, EncodeTime(GpsHr, GpsMi, GpsSc, GpsMs));
    If FTimeSynced Then Exit;
    IncMins := Trunc(FTimeZone * 60);
    Temp := FGPSTime + IncMins / MinsPerDay;
    DateTimeToSystemTime(Temp, ST);
    SetLocalTime(ST);
    FTimeSynced := FFixed;
End;

Procedure TGPSInfo.SetDate(DateStr: String);
Var
    Temp: TDateTime;
    GpsYr, GpsMn, GpsDy: Integer;
    IncMins: Integer;
    ST: TSystemTime;
Begin
    GpsYr := StrToInt(Copy(DateStr, 1, 4));
    GpsMn := StrToInt(Copy(DateStr, 5, 2));
    GpsDy := StrToInt(Copy(DateStr, 7, 2));
    FGPSTime := ComposeDateTime(EncodeDate(GpsYr, GpsMn, GpsDy), FGPSTime);
    If FTimeSynced Then Exit;
    IncMins := Trunc(FTimeZone * 60);
    Temp := FGPSTime + IncMins / MinsPerDay;
    DateTimeToSystemTime(Temp, ST);
    SetLocalTime(ST);
    FTimeSynced := FFixed;
End;

Procedure TGPSInfo.DecodeRMC(Params: TStringList);
Begin
    FGPSLock.Enter;
    Try
        If Params[0] <> '' Then SetTime(Params[0]);

        If Params[1] = 'A' Then FFixed := True
        Else FFixed := False;

        If Params[2] <> '' Then SetLatitude(Params[2], Params[3]);

        If Params[4] <> '' Then SetLongtitude(Params[4], Params[5]);

        If Params[6] <> '' Then SetSpdKNT(Params[6]);

        If Params[7] <> '' Then SetDirection(Params[7]);

        If Params[8] <> '' Then SetDate('20' + Copy(Params[8], 5,
                2) + Copy(Params[8], 3, 2) + Copy(Params[8], 1, 2));
    Finally
        FGPSLock.Leave;
    End;
End;

Procedure TGPSInfo.DecodeGGA(Params: TStringList);
Begin
    FGPSLock.Enter;
    Try

        If Params[0] <> '' Then SetTime(Params[0]);

        If Params[1] <> '' Then SetLatitude(Params[1], Params[2]);

        If Params[3] <> '' Then SetLongtitude(Params[3], Params[4]);

        If Params[5] <> '' Then
        Begin
            FFixQuality := fqInvalid;
            Inc(FFixQuality, StrToInt(Params[5]));
        End;

        If Params[6] <> '' Then FSatNumbers := StrToInt(Params[6]);

        If (Params[8] <> '') Then
        Begin
            If Params[10] <> '' Then FAltitude :=
                    StrToFloat(Params[8]) + StrToFloat(Params[10])
            Else FAltitude := StrToFloat(Params[8]);
        End;
    Finally
        FGPSLock.Leave;
    End;
End;

Procedure TGPSInfo.DecodeGSA(Params: TStringList);
Begin
    FGPSLock.Enter;
    Try
        If Params[1] <> '' Then
        Begin
            FFixMode := fmNoFix;
            Inc(FFixMode, StrToInt(Params[1]));
        End;

        //	If Params[14] <> '' Then
        //	FPDOP := StrToFloat(Params[14]);
        //
        //	If Params[15] <> '' Then
        //	FHDOP := StrToFloat(Params[15]);
        //
        //	If Params[16] <> '' Then
        //	FVDOP := StrToFloat(Params[16]);
    Finally
        FGPSLock.Leave;
    End;
End;

Procedure TGPSInfo.DecodeGLL(Params: TStringList);
Begin
    FGPSLock.Enter;
    Try
        If Params[0] <> '' Then SetLatitude(Params[0], Params[1]);

        If Params[2] <> '' Then SetLongtitude(Params[2], Params[3]);

        If Params[4] <> '' Then SetTime(Params[4]);

        If Params[5] <> 'A' Then FFixed := True;
    Finally
        FGPSLock.Leave;
    End;
End;

Procedure TGPSInfo.DecodeVTG(Params: TStringList);
Begin
    FGPSLock.Enter;
    Try
        If Params[6] <> '' Then SetSpdKMH(Params[6]);
    Finally
        FGPSLock.Leave;
    End;
End;

Procedure TGPSInfo.Parse(ANMEAString: String);
Var
    Idx: Integer;
    Params: TStringList;
    CMD: String;
Begin
    ANMEAString := Trim(ANMEAString);
    If not IsValidNMEA(ANMEAString) Then Exit;
    Idx := Pos('*', ANMEAString);
    ANMEAString := LeftStr(ANMEAString, Idx - 1);
    Params := TStringList.Create;
    Params.StrictDelimiter := True;
    Params.CommaText := ANMEAString;
    CMD := Copy(Params[0], 4, 3);
    Params.Delete(0);
    If CMD = 'VTG' Then DecodeVTG(Params);
    If CMD = 'GLL' Then DecodeGLL(Params);
    If CMD = 'GSA' Then DecodeGSA(Params);
    If CMD = 'GGA' Then DecodeGGA(Params);
    If CMD = 'RMC' Then DecodeRMC(Params);
    Params.Free;
End;

Procedure EnumComPort(Var ComPortList: TStringList);
Var
    Reg: TRegIniFile;
    RegList: TStringList;
    I: Integer;
Begin
    ComPortList.Clear;
    Reg := TRegIniFile.Create;
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    RegList := TStringList.Create;
    If not Reg.OpenKeyReadOnly('Drivers\BuiltIn\') Then Exit;
    Reg.ReadSections(RegList);
    Reg.CloseKey;
    For I := 0 To RegList.Count - 1 Do
    Begin
        If Reg.OpenKeyReadOnly('Drivers\BuiltIn\' + RegList[I] + '\') Then
        Begin
            If Reg.ValueExists('Prefix') and Reg.ValueExists('Port') Then
            Begin
                Reg.CloseKey;
                If (Reg.ReadString('Drivers\BuiltIn\' + RegList[I],
                    'Prefix', '') = 'COM') and
                    (Copy(Reg.ReadString('Drivers\BuiltIn\' + RegList[I],
                    'Port', ''), 1,
                    3) = 'COM') Then
                    ComPortList.Add(Reg.ReadString('Drivers\BuiltIn\' + RegList[I],
                        'Port', ''));
            End;
            Reg.CloseKey;
        End;
    End;
    Reg.CloseKey;
    RegList.Destroy;
    Reg.Destroy;
End;


End.
